﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Reflection;
using System.Web;
using System.Web.UI;
using System.Text;


using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Registers and executes a parametrized JavaScript command.
    /// </summary>
    /// <remarks><b>JsCommand</b> supports multiple records. The <b>Execute</b> methods builds a
    /// script in a loop until all records have been processed.</remarks>
    public class JsCommand : WebCommandBase
    {
        #region Constants.

        /// <summary>
        /// Optional context variable, which when set will cause the command to replace all "parent."
        /// ocurrences in the function call with the provided string.
        /// </summary>
        /// <remarks>Typically used when using an editable grid in the multi-row select mode with 
        /// <see cref="BoundLookupTemplate"/> column template.</remarks>
        public const string ContextVariable_ParentReplacementString = "JsCommand_ParentReplacementString";

        #endregion

        #region FieldPermissions and properties.

        string callFormat;

        /// <summary>
        /// Gets or sets JavaScript function call format. E.g. "window.opener.SetValues('{0}', '{1}')".
        /// </summary>
        protected string CallFormat
        {
            get { return this.callFormat; }
            set { this.callFormat = value; }
        }

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of the <b>JsCommand</b> class.
        /// </summary>
        protected JsCommand()
        { }

        /// <summary>
        /// Initializes a new instance of the <b>JsCommand</b> class.
        /// </summary>
        /// <param name="key">Command key. Nullable. Should be unique in the collection and language neutral.</param>
        /// <param name="text">Command description.</param>
        /// <param name="jsFunctionCall">JavaScript function call. E.g. "window.close();".</param>
        /// <remarks>Use this constructor overload if the command doesn't require parameters.</remarks>
        public JsCommand(string key, string text, string jsFunctionCall)
            : base(key, text)
        {
            this.callFormat = jsFunctionCall;
        }

        /// <summary>
        /// Initializes a new instance of the <b>JsCommand</b> class.
        /// </summary>
        /// <param name="key">Command key. Nullable. Should be unique in the collection and language neutral.</param>
        /// <param name="text">Command description.</param>
        /// <param name="callFormat">JavaScript function call format. E.g. "window.opener.SetValues('{0}', '{1}')".</param>
        /// <param name="requiredParameters">Array of parameters whose values are used to fill the callFormat.
        /// Must be ordered in the same fashion as the <b>callFromat</b> parameters.</param>
        public JsCommand(string key, string text, string callFormat, string[] requiredParameters)
            : base(key, text, requiredParameters)
        {
            this.callFormat = callFormat;
        }

        #endregion

        #region Execute.

        /// <summary>
        /// Executes the command with provided parameters.
        /// </summary>
        /// <param name="caller">Object/client which is calling the method.</param>
        /// <param name="actor">Command initiator. This may be an application user, software module, 3rd party 
        /// application etc. which is initiating the command.</param>
        /// <remarks><para>This overload should be used when the command should perform additional security
        /// checks (vertical/horizontal) or when the command execution must be logged. Preffer using
        /// this overload whenever the <see cref="IActor"/> object is available.</para></remarks>
        public override void Execute(object caller, IActor actor)
        {
            EnsureParameters(true);
            string functionCallFormat = GetCallFormatWithAppliedParentReplacementString();
            if (!functionCallFormat.Trim().EndsWith(";"))
                functionCallFormat += "; ";

            string[][] scriptParams = GetScriptParameters();
            bool noParamsProvided = (scriptParams.Length == 0);

            string script = "";
            if (noParamsProvided)
            {
                script = functionCallFormat;
            }
            else
            {
                script = GenerateClientScript(functionCallFormat, scriptParams);
            }

            JScript.RegisterStartupScript(script, true);
        }

        /// <summary>
        /// Generates JavaScript which processes provided parameters. Builds a script in a loop until all records have been processed.
        /// </summary>
        /// <param name="functionCallFormat">JavaScript function call format. E.g. "window.opener.SetValues('{0}', '{1}')".</param>
        /// <param name="scriptParams">Parameters defined by client.</param>
        /// <returns>Client script.</returns>
        protected virtual string GenerateClientScript(string functionCallFormat, string[][] scriptParams)
        {
            StringBuilder script = new StringBuilder();
            foreach (string[] functionParams in scriptParams)
                script.Append(string.Format(functionCallFormat, functionParams));

            return script.ToString();
        }

        private string[][] GetScriptParameters()
        {
            string[][] scriptParams = new string[this.Parameters.Count][];
            for (int idxRecord = 0; idxRecord < this.Parameters.Count; idxRecord++)
            {
                scriptParams[idxRecord] = new string[this.RequiredParameters.Length];
                for (int idxPar = 0; idxPar < this.RequiredParameters.Length; idxPar++)
                {
                    object parValue = GetParameterValue(idxRecord, this.RequiredParameters[idxPar]);
                    string parText = (this.ParameterFormatter != null) ? this.ParameterFormatter.Format(parValue) : Convert.ToString(parValue);
                    parText = parText.Replace(@"\", @"\\");
                    parText = parText.Replace(@"'", @"\'");
                    parText = parText.Replace("\"", "\\\"");

                    scriptParams[idxRecord][idxPar] = parText;
                }
            }

            return scriptParams;
        }

        private string GetCallFormatWithAppliedParentReplacementString()
        {
            ReadContextVariables(ContextVariable_ParentReplacementString);

            string functionCall = this.callFormat;
            if (this.ContextVariables.ContainsKey(ContextVariable_ParentReplacementString))
            {
                string variableValue = this.ContextVariables[ContextVariable_ParentReplacementString];
                string parentReplacement = (variableValue.StartsWith("parent.")) ? variableValue : "parent." + variableValue;
                functionCall = functionCall.Replace("parent.", parentReplacement);
            }

            return functionCall;
        }

        #endregion

        #region ExecuteOnClient.

        /// <summary>
        /// Gets the script generated by the command which starts it's execution in the Web browser.
        /// </summary>
        /// <remarks>Commands which don't require parameters will be entirely executed in the Web browser
        /// and will not invoke server side method.</remarks>
        public override string ExecuteOnClient
        {
            get
            {
                string script = "";
                if (!string.IsNullOrEmpty(this.PrependToExecuteOnClient))
                    script += this.PrependToExecuteOnClient;

                if (this.RequiredParameters.Length == 0)
                    script += GetCallFormatWithAppliedParentReplacementString();
                else
                    script += this.InvokeServerSideExecution;

                if (!string.IsNullOrEmpty(this.AppendToExecuteOnClient))
                    script += this.AppendToExecuteOnClient;

                return script;
            }
        }

        #endregion

        #region Clone, CopyFrom.

        /// <summary>
        /// Creates a new object that is a copy of the current instance. 
        /// </summary>
        /// <remarks>Must override in derived classes.</remarks>
        public override object Clone()
        {
            if (GetType() != typeof(JsCommand))
                throw new InvalidOperationException("Clone method must be overriden in derived classes. Do not use \"base.Clone()\" in derived class.");

            JsCommand clone = new JsCommand();
            clone.CopyFrom(this);
            return clone;
        }

        /// <summary>
        /// Copies information from an <b>JsCommand</b> object. 
        /// </summary>
        /// <param name="source">The <b>JsCommand</b> object to base the new object on.</param>
        /// <remarks>This method copies information to an JsCommand object contained in the specified 
        /// JsCommand object. Override in derived classes if new class fields/members are introduced.</remarks>
        protected virtual void CopyFrom(JsCommand source)
        {
            base.CopyFrom(source);
            this.callFormat = source.callFormat;
        }

        #endregion
    }
}